1 module server; 2 import handy_httpd; 3 import websocket_connection; 4 public import websocket_connection: pushWebsocketMessage; 5 import core.sync.semaphore; 6 import commons; 7 8 enum DEFAULT_PATH="build"; 9 private __gshared string startPath; 10 private __gshared ushort port = 9000; 11 12 13 int hipengineStartServer(string[] args, shared ushort* serverPort, shared string* serverHost, string servePath, shared Semaphore sem) 14 { 15 import std.stdio; 16 import myip; 17 import handy_httpd.handlers.path_handler; 18 import core.thread; 19 import std.socket; 20 21 import slf4d.default_provider; 22 import slf4d; 23 24 auto provider = new DefaultProvider(false, Levels.ERROR); 25 26 string privateIp = "127.0.0.1"; 27 foreach(addr; privateAddresses) 28 { 29 if(isIpAddress(addr)) 30 { 31 privateIp = addr; 32 break; 33 } 34 } 35 configureLoggingProvider(provider); 36 ServerConfig cfg; 37 cfg.hostname = "0.0.0.0"; 38 cfg.port = port; 39 cfg.workerPoolSize = 3; 40 cfg.enableWebSockets = true; // Important! Websockets won't work unless `enableWebSockets` is set to true! 41 WebSocketHandler ws = new WebSocketHandler(new WebSocketReloadServer()); 42 PathHandler pathHandler = new PathHandler() 43 .addMapping(Method.GET, "/ws", ws) 44 .addMapping("/**", &serveGameFiles); 45 46 47 *serverPort = port; 48 *serverHost = privateIp.dup; 49 startPath = servePath; 50 writeln("HipremeEngine Dev Server listening from ",privateAddresses,":", port, " path ", startPath); 51 (cast()sem).notify; 52 53 new HttpServer(pathHandler, cfg).start(); 54 55 return 0; 56 } 57 58 59 private __gshared HttpRequestContext* context; 60 61 void serveGameFiles(ref HttpRequestContext ctx) 62 { 63 synchronized 64 { 65 if(context is null) 66 context = &ctx; 67 } 68 import std.path; 69 import std.stdio; 70 import std.file; 71 import std.string; 72 import std.conv; 73 74 if(ctx.request.method != Method.GET) 75 return; 76 77 string path = ctx.request.url.length > 0 ? ctx.request.url : "/"; 78 string targetPath = buildNormalizedPath(startPath, DEFAULT_PATH); 79 string contentType = contentTypeFromFileExtension(path); 80 string file = buildNormalizedPath(targetPath, path[1..$]); 81 82 83 string result; 84 85 if(path == "/") 86 { 87 string indexHTML = buildNormalizedPath(targetPath, "index.html"); 88 contentType = "text/html; charset=utf8"; 89 if(exists(indexHTML)) 90 { 91 import std.string; 92 string reloadServer = replace(import("reload_server.js"), "$WEBSOCKET_SERVER$", "ws://localhost:"~port.to!string~"/ws"); 93 result = readText(indexHTML)~"<script> "~reloadServer~"</script>"; 94 } 95 else 96 { 97 // index 98 string html = "<html><head><title>Hipreme Engine Webassembly Server</title></head><body><ul>"; 99 foreach(string name; dirEntries(targetPath, SpanMode.shallow)) 100 { 101 name = name[targetPath.length..$]; 102 html ~= "<li><a href=\"" ~ name ~ "\">" ~ name ~"</a></li>"; 103 } 104 html~= "</body></html>"; 105 result = html; 106 } 107 } 108 else if(contentType) 109 { 110 if(!exists(file)) 111 { 112 ctx.response.status = HttpStatus.NOT_FOUND; 113 result = "404 file not found"; 114 } 115 else 116 { 117 ctx.response.addHeader("Access-Control-Expose-Headers", "Content-Length"); 118 ctx.response.status = HttpStatus.OK; 119 if(contentType.startsWith("text")) 120 result = readText(file); 121 else 122 result = cast(string)read(file); 123 writeln("GET ", file, " 200"); 124 } 125 } 126 127 128 ctx.response.writeBodyString(result, contentType); 129 130 } 131 132 string contentTypeFromFileExtension(string filename) 133 { 134 import std.path; 135 string ext = filename.extension; 136 switch(ext) 137 { 138 case ".png": 139 return "image/png"; 140 case ".apng": 141 return "image/apng"; 142 case ".svg": 143 return "image/svg+xml"; 144 case ".jpg": 145 return "image/jpeg"; 146 case ".html": 147 return "text/html"; 148 case ".css": 149 return "text/css"; 150 case ".js": 151 return "application/javascript"; 152 case ".wasm": 153 return "application/wasm"; 154 case ".mp3": 155 return "audio/mpeg"; 156 case ".pdf": 157 return "application/pdf"; 158 case ".ogg": 159 return "application/ogg"; 160 case ".mp4": 161 return "application/mp4"; 162 default: 163 return "application/octet-stream"; 164 } 165 } 166 167 void stopServer() 168 { 169 if(context is null) 170 return; 171 pushWebsocketMessage("close"); 172 context.server.stop(); 173 }